﻿//
// Copyright (c) 2011 HyperCrunch, Inc. http://www.hypercrunch.com 
// Author: Nik Kalyani  http://www.kalyani.com (Twitter: @techbubble)
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation 
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and 
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions 
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF 
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
// DEALINGS IN THE SOFTWARE.
//

using System;
using System.IO;
using System.Xml.Serialization;
using System.Xml.XPath;
using DotNetNuke.Services.Cache;
using DotNetNuke.Common.Utilities;
using DotNetNuke.Entities.Portals;
using System.Text;
using System.Collections.Generic;
using System.Xml;
using System.Collections;
using System.Reflection;

namespace HyperCrunch.Druid
{

    [Serializable(), XmlRoot("RuleDefinition")]
    public class RuleDefinition
    {

        #region "Private Members"

        private RuleCollection _rules;

        #endregion

        #region "Public Properties"

        public RuleCollection Rules
        {
            get { return _rules; }
            set { _rules = value; }
        }

        #endregion

        #region "Shared Methods"


        public static RuleDefinition MergeRuleDefinition(RuleDefinition hostDefinition, string filePath, string cacheKey)
        {
            if (hostDefinition == null)
            {
                hostDefinition = new RuleDefinition();
                hostDefinition.Rules = new RuleCollection();
            }

            // Try and get a portal rules definition from cache, otherwise
            // read from file
            RuleDefinition portalDefinition = (RuleDefinition)DataCache.GetCache(cacheKey);
            if (portalDefinition == null)
            {
                portalDefinition = GetRuleDefinition(filePath, cacheKey, false);
                if (portalDefinition != null)
                    DataCache.SetCache(cacheKey, portalDefinition, new DNNCacheDependency(filePath));
            }

            // If a portal definition exists, perform a merge with the host definition rules
            if (portalDefinition != null)
            {
                foreach (Rule rule in portalDefinition.Rules)
                {
                    int matchIndex = hostDefinition.Rules.FindMatchingId(rule.Id);

                    // If a duplicate rule exists, override the Host rule if permitted, otherwise ignore it
                    if (matchIndex > -1)
                    {
                        if (hostDefinition.Rules[matchIndex].AllowOverride == AllowOverrideType.Always)
                            hostDefinition.Rules[matchIndex] = rule;
                    }
                    else
                        hostDefinition.Rules.Add(rule); // Add a unique rule
                }
            }

            hostDefinition.Rules.SortByOrder();
            return hostDefinition;
        }

        public static RuleDefinition GetRuleDefinition(string filePath, string cacheKey, bool isHostRule)
        {

            RuleDefinition definition = null;
            if (isHostRule)
                definition = (RuleDefinition)DataCache.GetCache(cacheKey);
            else
                definition = null;

            if ((definition == null))
            {
                definition = new RuleDefinition();

                if (isHostRule && (!File.Exists(filePath)))
                {
                    try
                    {
                        string config = "";
                        string resource = "HyperCrunch.Druid.assets." + Globals.DRUID_CONFIG_FILE;
                        using (Stream stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(resource))
                        using (StreamReader reader = new StreamReader(stream))
                        {
                            config = reader.ReadToEnd();
                        }
                        File.WriteAllText(filePath, config);
                    }
                    catch
                    {
                    }
                }

                definition.Rules = GetRules(filePath, isHostRule);

                if (definition.Rules.Count == 0) return null;

                if (isHostRule)
                    DataCache.SetCache(cacheKey, definition, new DNNCacheDependency(filePath));
            }

            return definition;
        }

        private static void SortDocument(XmlDocument doc)
        {
            XmlNode root = doc.SelectSingleNode("/Rules");
            Sort(root);
        }

        private static void Sort(XmlNode root)
        {
            XmlNodeList nodes = root.SelectNodes("Rule");
            List<XmlNode> sortedNodes = new List<XmlNode>();
            foreach (XmlNode node in nodes)
            {
                Sort(node);
                sortedNodes.Add(node);
            }

            sortedNodes.Sort(new OrderSortHelper());
            foreach (XmlNode node in sortedNodes)
                root.AppendChild(node);
        }

        private class OrderSortHelper: IComparer<XmlNode>
        {
            public int Compare(XmlNode r1, XmlNode r2)
            {
                int order1 = r1.Attributes[Globals.ORDER_ATTRIBUTE] == null ? 1000000 : Convert.ToInt32(r1.Attributes[Globals.ORDER_ATTRIBUTE].Value);
                int order2 = r2.Attributes[Globals.ORDER_ATTRIBUTE] == null ? 1000000 : Convert.ToInt32(r2.Attributes[Globals.ORDER_ATTRIBUTE].Value);

                return order1.CompareTo(order2);
            }
        }

        public static void UpdateRule(string filePath, string ruleId, Hashtable properties)
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(filePath);

            XmlNode node = doc.SelectSingleNode(@"descendant-or-self::*[@id='" + ruleId + "']");
            node.Attributes.RemoveAll();
            foreach (string property in properties.Keys)
            {
                string propValue = properties[property].ToString();
                if (!String.IsNullOrEmpty(propValue))
                    node.Attributes.Append(GetAttribute(doc, Rule.GetAttributeForProperty(property), propValue));
            }
            SortDocument(doc); 
            doc.Save(filePath);
        }

        public static void InsertRule(string filePath, string parentRuleId, Hashtable properties)
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(filePath);

            XmlNode newNode = doc.CreateElement("Rule");
            foreach (string property in properties.Keys)
            {
                string propValue = properties[property].ToString();
                if (!String.IsNullOrEmpty(propValue))
                    newNode.Attributes.Append(GetAttribute(doc, Rule.GetAttributeForProperty(property), propValue));
            }
            if (parentRuleId == String.Empty)
            {
                XmlNode node = doc.SelectSingleNode("/Rules");
                node.PrependChild(newNode);
            }
            else
            {
                XmlNode node = doc.SelectSingleNode(@"descendant-or-self::*[@id='" + parentRuleId + "']");
                node.PrependChild(newNode);
            }
            SortDocument(doc); 
            doc.Save(filePath);
        }

        public static void DeleteRule(string filePath, string ruleId, Hashtable properties)
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(filePath);

            XmlNode node = doc.SelectSingleNode(@"descendant-or-self::*[@id='" + ruleId + "']");
            node.ParentNode.RemoveChild(node);

            doc.Save(filePath);
        }


        public static void MoveRule(string filePath, string sourceRuleId, string targetRuleId)
        {
            XmlDocument doc = new XmlDocument();
            doc.Load(filePath);

            XmlNode sourceNode = doc.SelectSingleNode(@"descendant-or-self::*[@id='" + sourceRuleId + "']");   
            XmlNode targetNode;
            
            if (String.IsNullOrEmpty(targetRuleId))
                targetNode = doc.SelectSingleNode("/Rules");
            else
                targetNode = doc.SelectSingleNode(@"descendant-or-self::*[@id='" + targetRuleId + "']");

            targetNode.AppendChild(sourceNode);

            SortDocument(doc);
            doc.Save(filePath);
        }

        public static void MoveRuleToRoot(string filePath, string ruleId)
        {
            MoveRule(filePath, ruleId, "");
        }

        private static XmlAttribute GetAttribute(XmlDocument doc, string attrName, string attrValue)
        {
            XmlAttribute attribute = doc.CreateAttribute(attrName);
            attribute.Value = attrValue;
            return attribute;
        }

        public static RuleCollection GetRules(string filePath, bool isHostRule)
        {
            RuleCollection rules = new RuleCollection();
            if (File.Exists(filePath))
            {
                //Create a FileStream for the Config file
                FileStream fileReader = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);

                XPathDocument doc = new XPathDocument(fileReader);
                int counter = 0;

                foreach (XPathNavigator nav in doc.CreateNavigator().Select("Rules/Rule"))
                {
                    Rule rule = GetRule(nav, isHostRule, counter++);
                    if (rule != null)
                        rules.Add(rule);
                }

                if (fileReader != null)
                    fileReader.Close();

            }
            return rules;
        }

        private static Rule GetRule(XPathNavigator nav, bool isHostRule, int counter)
        {

            string id = nav.GetAttribute(Globals.ID_ATTRIBUTE, "");
            if (String.IsNullOrEmpty(id)) return null;
            
            Rule rule = new Rule();
            rule.Id = id;
            rule.AllowOverride = !String.IsNullOrEmpty(nav.GetAttribute(Globals.ALLOW_OVERRIDE_ATTRIBUTE, "")) ? (AllowOverrideType)Enum.Parse(typeof(AllowOverrideType), nav.GetAttribute(Globals.ALLOW_OVERRIDE_ATTRIBUTE, "")) : AllowOverrideType.Never;
            rule.ContinueAfterMatch = !String.IsNullOrEmpty(nav.GetAttribute(Globals.CONTINUE_AFTER_MATCH_ATTRIBUTE, "")) ? (ContinueAfterMatchType)Enum.Parse(typeof(ContinueAfterMatchType), nav.GetAttribute(Globals.CONTINUE_AFTER_MATCH_ATTRIBUTE, "")) : ContinueAfterMatchType.Never;
            rule.MatchField = nav.GetAttribute(Globals.MATCH_FIELD_ATTRIBUTE, "");
            rule.MatchValues = nav.GetAttribute(Globals.MATCH_VALUES_ATTRIBUTE, "");
            rule.MatchCondition = !String.IsNullOrEmpty(nav.GetAttribute(Globals.MATCH_CONDITION_ATTRIBUTE, "")) ? (MatchConditionType)Enum.Parse(typeof(MatchConditionType), nav.GetAttribute(Globals.MATCH_CONDITION_ATTRIBUTE, "")) : MatchConditionType.Equal;
            rule.MatchType = nav.GetAttribute(Globals.MATCH_TYPE_ATTRIBUTE, "");
            rule.Debug = !String.IsNullOrEmpty(nav.GetAttribute(Globals.DEBUG_ATTRIBUTE, "")) ? nav.GetAttribute(Globals.DEBUG_ATTRIBUTE, "") :  null;
            rule.IgnoreIf = !String.IsNullOrEmpty(nav.GetAttribute(Globals.IGNORE_IF_ATTRIBUTE, "")) ? (IgnoreConditionType)Enum.Parse(typeof(IgnoreConditionType), nav.GetAttribute(Globals.IGNORE_IF_ATTRIBUTE, "")) : IgnoreConditionType.Never;
            rule.IsHostRule = isHostRule;
            try
            {
                rule.Order = Convert.ToInt32(nav.GetAttribute(Globals.ORDER_ATTRIBUTE, ""));
            }
            catch
            {
                rule.Order = counter;
            }

            rule.SkinControl = nav.GetAttribute(Globals.DATA_SKIN_CONTROL_ATTRIBUTE,"");
            rule.ContainerControl = nav.GetAttribute(Globals.DATA_CONTAINER_CONTROL_ATTRIBUTE,"");
            rule.Theme = nav.GetAttribute(Globals.DATA_THEME_ATTRIBUTE, "");
            rule.ContentRenderer = nav.GetAttribute(Globals.DATA_CONTENT_RENDERER_ATTRIBUTE, "");
            rule.ContentModifier = nav.GetAttribute(Globals.DATA_CONTENT_MODIFIER_ATTRIBUTE, "");
            rule.StylesheetUrl = nav.GetAttribute(Globals.DATA_STYLESHEET_URL_ATTRIBUTE, "");
            rule.ScriptUrl = nav.GetAttribute(Globals.DATA_SCRIPT_URL_ATTRIBUTE, "");
            rule.RedirectUrl = nav.GetAttribute(Globals.DATA_REDIRECT_URL_ATTRIBUTE, "");
            rule.JsonSettingName = nav.GetAttribute(Globals.JSON_SETTING_NAME_ATTRIBUTE, "");
            rule.JsonSettingValue = nav.GetAttribute(Globals.JSON_SETTING_VALUE_ATTRIBUTE, "");


            if (!String.IsNullOrEmpty(rule.MatchType))
            {
                XPathNodeIterator childRules = nav.Select("Rule");
                if (childRules.Count > 0)
                {
                    rule.Rules = new RuleCollection();
                    int childCounter = 0;
                    foreach(XPathNavigator childNav in childRules)
                    {
                        Rule childRule = GetRule(childNav, isHostRule, childCounter++);
                        if (childRule != null)
                            rule.Rules.Add(childRule);
                    }
                }
                return rule;
            }
            else
                return null;
        }

        private static string DebugPrint(RuleCollection rules, int indent)
        {
            StringBuilder rulesString = new StringBuilder();

            foreach (Rule r in rules)
            {
                rulesString.Append("<div style='margin-left:" + String.Format("{0:###0px}", indent * 10) + "'>" + r.Id + " (" + r.Order.ToString() + ")</div>");
                if (r.Rules != null)
                    rulesString.Append(DebugPrint(r.Rules, indent + 1));
            }

            return rulesString.ToString();
        }

        public static void SaveRuleDefinition(string rulesetName, RuleDefinition definition)
        {

            string cacheKey = rulesetName + "." + PortalSettings.Current.PortalId.ToString();

                
            // Create a new Xml Serializer
            XmlSerializer ser = new XmlSerializer(typeof(RuleDefinition));
            string filePath = "";

            //Create a FileStream for the Config file
            filePath = PortalSettings.Current.HomeDirectoryMapPath + rulesetName + ".config";
            if (File.Exists(filePath))
            {
                // make sure file is not read-only
                File.SetAttributes(filePath, FileAttributes.Normal);
            }
            FileStream fileWriter = new FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.Write);

            // Open up the file to serialize
            StreamWriter writer = new StreamWriter(fileWriter);

            // Serialize the Configuration
            ser.Serialize(writer, definition);

            // Close the Writers
            writer.Close();
            fileWriter.Close();

            // Set Cache
            DataCache.SetCache(cacheKey, definition, new DNNCacheDependency(filePath));

        }

        #endregion

    }
}

